大家好,我是 Yubin
這邊文章將介紹把 Fastify App 包成 Image 的方法與注意事項。
假設我們手上有一個 Fastify 的專案,為了部屬的準備,我想把這個 Fastify App 包成 Image,有了這個 Image 我就可以用 Docker, Podman, Kubernetes 等工具或平台進行部屬。
如果手上沒有 Fastify App 的朋友,可以參考 Fastify101: Hello World 建立一個基本的 Fastify 專案。或參考本篇文章最底下的完整專案連結。
以下皆使用 docker 指令為範例,如果使用 podman,只要把指令置換掉就好。
如:docker build
->podman build
等等。
要建立 image,我們需要撰寫 Dockerfile。
Dockerfile 是一些指令的集合,用來定義這個 image 如何組成。
在專案根目錄建立一個 Dockerfile
的檔案:
FROM node:18.10.0
WORKDIR /app
COPY . /app
RUN npm install
RUN npm run build
CMD npm run start
FROM
定義 base image,也就是這個 image 要以哪個 image 為基底去生成。我這邊使用的是 node:18.10.0
這個 image,可以根據專案所用的版本或環境挑選適合的 tag。
WORKDIR
定義工作目錄,也就是設定 container 開始啟動之後,預設的工作目錄在哪裡。
COPY
把本機的檔案複製進 image 的生成環境中。這邊 COPY . /app
指的是把我的當前目錄的所有檔案及目錄 (連同子目錄) 複製進 image 裡面的 /app 目錄底下。
RUN
執行某個指令。
CMD
定義這個 image 啟動後,container 之後所執行的指令,如果這個指令執行結束或被關閉,container 也跟著被關閉。
Dockerfile 寫完,就可以把它 build 成 image。
docker build -t my-app:0.0.1 .
-t
為這個 image 定義 tag,這邊定義這個 image 叫做 my-app
,tag 名稱是 0.0.1。
.
後面有一個點,是在說 Dockerfile 放在哪個目錄底下,因為我現在跟 Dockerfile 在同個目錄,所以寫一個點 (.
)。
成功執行完沒有錯誤訊息後,可以執行 docker images
看一下剛剛建立的 image。
有了 image 之後,接這把這個 image 跑起來試試。
docker run -d -p 8888:8888 my-app:0.0.1
-d
detach,將當於在背景執行。
-p
把我本機上的 8888 Port 跟 Container 裡面的 8888 Port 綁在一起。(因為我知道程式會聽在 8888 Port)
執行完會顯示 Container 的 ID,可以利用 docker ps
來查看。
接著就可以打開瀏覽器或 Postman 打打看我們包出來的 Fastify App。
至此我們就完成了 Fastify App 的容器化。
但是,一個小小 App 的 image 就佔了 1G 多的空間實在說不過去。
這邊我們可以換個適合的 base image。
Alpine 是一個更為輕量化的 Linux 專案。
我們試著選用 alpine 版本的 node image 來包。
FROM node:18.10.0-alpine
WORKDIR /app
COPY . /app
RUN npm install
RUN npm run build
CMD npm run start
使用的是 node:18.10.0-alpine
的 image。
一樣執行 build 的指令。
docker build -t my-app:0.0.2 .
為了方便比較,把 tag 設定為 0.0.2
。
build 完,執行 docker images
來觀察。
換個 base image 就可以有明顯的差距。
我們上面的步驟中,使用 COPY . /app
把所有檔案都複製進 image 中,然後進行 npm install
與 npm run build
的動作,來將 TypeScript 的程式碼,編譯成 JavaScript 的執行檔。
但,我們的 src/
還存在在裡面,但對我們部屬有用的只有編譯出來的 dist/
目錄。
這邊我們可以透過 Dockerfile 的 Multi-stage 功能,build 完後只留下 build 出來的產物。
# Stage-1 for build
FROM node:18.10.0 as builder
WORKDIR /app
COPY . /app
RUN npm install
RUN npm run build
# Stage-2 for deployment
FROM node:18.10.0-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY package.json .
RUN npm install
CMD npm run start
可以看到,我們讓 Stage-1 當作我們用來 build 的工具,build 完之後,在 Stage-2 選用比較輕量的 image,再透過 COPY --from
來把 Stage-1 產出的執行檔複製過來。
如此一來,我們最後部屬出去的 image,裡面就不會記錄我們的 Source code。
而且用來 build 的那個 stage,也可以不太計較 image 的大小。因為許多情境下,在 build 階段會需要更多的相依套件。
docker build -t my-app:0.0.3 .
這邊把 tag 設為 0.0.3
。
上面的 Dockerfile 中,我們都是使用 npm install
來安裝相依套件。
但我們在開發的時候,許多專案都是 only for development 使用的,像是 TypeScript 或 Prettier(排版工具) 等等。
就是安裝的時候加上
-D
安裝的那些套件。
如npm i -D typescript
。
許多套件只是開發時期的工具,卻無助於我們程式在 Production 環境的運行。
使用 npm install
會根據 package.json 中定義的套件來進行安裝,有一些參數可以參考 npm 官方文件。
其中,使用 --omit=dev
參數,可以讓我們不要安裝 devDependencies
區塊的套件。
npm install --omit=dev
我們可以使用這個參數,來進一步精簡最後要上 Production 的那個 Stage。
# Stage-1 for build
FROM node:18.10.0 as builder
WORKDIR /app
COPY . /app
RUN npm install
RUN npm run build
# Stage-2 for deployment
FROM node:18.10.0-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY package.json .
RUN npm install --omit=dev
CMD npm run start
再次執行 build 的指令。
docker build -t my-app:0.0.4 .
這邊把 tag 設為 0.0.4
。
可以看到少掉開發用的套件,我們最後產出的 image 變得更加輕量。
所以正確的定義套件是不是開發時期的相依套件很重要。
本文介紹了 Dockerfile 的撰寫,及一些讓 image 輕量的基本方法。
特別要提的是,要把 build 跟 deployment 分開,Dockerfile 的 Multi-stage 只是一種方法,更多人採用的方式可能是在 CI Pipeline 中定義負責 Build Source code 的階段跟 Build Image 的階段。概念上也是把 Stage 拆開,Dockerfile 只是一種可行的解,但不是唯一解。
要精簡 image 的大小還有許多方式跟細節,本文以 npm 的使用為主軸來跟各位分享。
在包 image 的時候,也要特別留意 .dockerignore 的正確定義,避免不小心把一些垃圾一起包進 image。
關於許多撰寫 Dockerfile 上的 Security 問題,可以參考這篇文章。
在 Cloud Native 的世界,到處都是 Container,在有 Container 之前,image 的產生是非常重要的,大家要謹慎面對。
以上範例專案,可以參考 GitHub。